/*
 * ExecTestSuite.java  0.4.0 / Oct 25, 2010
 *
 * Copyright (c) 2010 - 2011, cute4j.com and contributors.
 *
 * This software is licensed under the LGPL version 2.1 license.
 * A copy of this license is included in the file LICENSE-LGPL.txt.
 */
package com.cute4j.nunit;

import java.io.BufferedReader;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;

import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;

/**
 * Test suite that executes each method which name starts with "testExec" in a
 * new separate JVM.
 *
 * @author Slobodan Vrkacevic
 * @version 0.4.0
 */
public class ExecTestSuite extends TestSuite {

// Private Fields. -------------------------------------------------------------

  private final Object lock;
  private Process jvm;

// Public Constructors. --------------------------------------------------------

  /**
   * Constructs new ExecTestSuite object.
   */
  public ExecTestSuite() {
    super();
    lock = new Object();
  }

  /**
   * Constructs new ExecTestSuite object.
   *
   * @param name the test suite name
   */
  public ExecTestSuite(String name) {
    super(name);
    lock = new Object();
  }

  /**
   * Constructs new ExecTestSuite object.
   *
   * @param c the test suite class
   */
  public ExecTestSuite(Class c) {
    super(c);
    lock = new Object();
  }

  /**
   * Constructs new ExecTestSuite object.
   *
   * @param c the test suite class
   * @param name the test suite name
   */
  public ExecTestSuite(Class c, String name) {
    super(c, name);
    lock = new Object();
  }

// Public Methods. -------------------------------------------------------------

  /**
   * Stops test running.
   */
  public void stopTest() {
    synchronized (lock) {
      if (jvm != null) {
        jvm.destroy();
      }
    }
  }

// Protected Methods. ----------------------------------------------------------

  /**
   * Creates a temporary file for the error stream.
   *
   * @return a temporary file for the error stream
   */
  protected File createErrorStreamFile() {
    try {
      return File.createTempFile("Cute4jNUnit_ErrorStream", ".test");
    } catch (IOException e) {
      return new File("Cute4jNUnit_ErrorStream.test");
    }
  }

  /**
   * Returns a temporary file for the failure stream.
   *
   * @return a temporary file for the failure stream
   */
  protected File createFailureStreamFile() {
    try {
      return File.createTempFile("Cute4jNUnit_FailureStream", ".test");
    } catch (IOException e) {
      return new File("Cute4jNUnit_FailureStream.test");
    }
  }

  /**
   * Executes specified test in a separate JVM.
   *
   * @param test the test
   * @param result the test result
   */
  protected void execTest(TestCase test, TestResult result) { // NOPMD
    File dir = new File(System.getProperty("user.dir"));
    File errorFile = createErrorStreamFile();
    File failureFile = createFailureStreamFile();
    InputStreamListener errorListener = null;
    InputStreamListener outputListener = null;
    deleteStreamFiles(errorFile, failureFile);
    String command;
    if (test instanceof NativeTest) {
      NativeTest nativeTest = (NativeTest)test;
      command = TestEnvironment.getLaunchCommand() +
                ExecTestSuite.class.getName() + TestEnvironment.STRING_SPACE +
                nativeTest.moduleName + TestEnvironment.STRING_SPACE +
                nativeTest.testCaseName + TestEnvironment.STRING_SPACE +
                test.getName() + TestEnvironment.STRING_SPACE +
                errorFile.getAbsolutePath() + TestEnvironment.STRING_SPACE +
                failureFile.getAbsolutePath();
    } else {
      command = TestEnvironment.getLaunchCommand() +
                ExecTestSuite.class.getName() + TestEnvironment.STRING_SPACE +
                test.getClass().getName() + TestEnvironment.STRING_SPACE +
                test.getName() + TestEnvironment.STRING_SPACE +
                errorFile.getAbsolutePath() + TestEnvironment.STRING_SPACE +
                failureFile.getAbsolutePath();
    }
    int exitValue;
    try {
      synchronized (lock) {
        jvm = TestEnvironment.execProcess(command, dir);
        errorListener = new InputStreamListener(jvm.getErrorStream());
        outputListener = new InputStreamListener(jvm.getInputStream());
        errorListener.start();
        outputListener.start();
      }
      exitValue = jvm.waitFor();
    } catch (Throwable t) {
      t.printStackTrace(); // NOPMD
      exitValue = -1;
    } finally {
      if (errorListener != null) {
        errorListener.stopListening();
      }
      if (outputListener != null) {
        outputListener.stopListening();
      }
    }
    synchronized (lock) {
      if (jvm != null) {
        printInputStream(jvm.getErrorStream(), System.err);
        printInputStream(jvm.getInputStream(), System.out);
      }
      jvm = null;
    }
    result.startTest(test);
    if (exitValue == 0) {
      if (errorFile.exists()) {
        ObjectInputStream eoos = null;
        try {
          eoos = new ObjectInputStream(new FileInputStream(errorFile));
          for (Throwable t = (Throwable)eoos.readObject(); t != null;
               t = (Throwable)eoos.readObject()) {
            result.addError(test, t);
          }
        } catch (EOFException eofe) { // NOPMD
          // empty catch block
        } catch (Throwable t) {
          System.err.println("Deserialization failed"); // NOPMD
          t.printStackTrace(); // NOPMD
        } finally {
          TestEnvironment.closeStream(eoos);
        }
      }
      if (failureFile.exists()) {
        ObjectInputStream foos = null;
        try {
          foos = new ObjectInputStream(new FileInputStream(failureFile));
          for (AssertionFailedError e = (AssertionFailedError)foos.readObject();
               e != null; e = (AssertionFailedError)foos.readObject()) {
            result.addFailure(test, e);
          }
        } catch (EOFException eofe) { // NOPMD
          // empty catch block
        } catch (Throwable t) { // NOPMD
          System.err.println("Deserialization failed"); // NOPMD
          t.printStackTrace(); // NOPMD
        } finally {
          TestEnvironment.closeStream(foos);
        }
      }
      if ((errorListener != null) && errorListener.hasContent()) {
        result.addFailure(test, new AssertionFailedError(
                                        errorListener.getContent()));
      }
    } else {
      result.addError(test,
                      new RuntimeException("Test process failed")); // NOPMD
    }
    if ((outputListener != null) && outputListener.hasContent()) {
      System.out.println(outputListener.getContent()); // NOPMD
    }
    result.endTest(test);
    deleteStreamFiles(errorFile, failureFile);
  }

// Overridden Methods. ---------------------------------------------------------

  /**
   * {@inheritDoc}
   */
  public void addTestSuite(Class testClass) {
    addTest(new ExecTestSuite(testClass));
  }

  /**
   * {@inheritDoc}
   */
  public void runTest(Test test, TestResult result) { // NOPMD
    if (test instanceof TestCase) {
      TestCase testCase = (TestCase)test;
      String testCaseName = testCase.getName();
      if (testCaseName != null) {
        int iof = testCaseName.indexOf("Fail");
        if ((iof == 4) || (iof == 8)) {
          result = new FailExpectedTestResult(result);
        }
        if (testCaseName.startsWith("testExec")) {
          execTest(testCase, result);
          return;
        }
      }
    }
    super.runTest(test, result);
  }

// Private Static Methods. -----------------------------------------------------

  private static void execNativeTest(String moduleName, String testCaseName,
                                     String testName, String errorFileName,
                                     String failureFileName) {
    try {
      Test test = NativeTest.findTest(moduleName, testCaseName, testName);
      if (test == null) {
        System.err.println("Could not find native test"); // NOPMD
        System.exit(-1); // NOFB
      } else {
        TestResult testResult = new TestResult();
        ExecTestListener etl = new ExecTestListener(testResult, errorFileName,
                                                    failureFileName);
        testResult.addListener(etl);
        test.run(testResult);
      }
    } catch (Throwable t) {
      t.printStackTrace(); // NOPMD
      System.exit(-1); // NOFB
    }
  }

  private static void execTest(String testClassName, String testName,
                               String errorFileName, String failureFileName) {
    try {
      Class testClass = Class.forName(testClassName);
      Test test = createTest(testClass, testName);
      TestResult testResult = new TestResult();
      ExecTestListener etl = new ExecTestListener(testResult, errorFileName,
                                                  failureFileName);
      testResult.addListener(etl);
      test.run(testResult);
    } catch (Throwable t) {
      t.printStackTrace(); // NOPMD
      System.exit(-1); // NOFB
    }
  }

  private static void printInputStream(InputStream is, PrintStream ps) {
    if ((is != null) && (ps != null)) {
      BufferedReader isr = null;
      try {
        isr = new BufferedReader(new InputStreamReader(is));
        for (String nl = isr.readLine(); nl != null; nl = isr.readLine()) {
          ps.println(nl);
        }
      } catch (IOException ioe) {
        System.err.println("Print input stream error"); // NOPMD
        ioe.printStackTrace(); // NOPMD
      } finally {
        TestEnvironment.closeReader(isr);
      }
    }
  }

  private static void deleteStreamFiles(File errorFile, File failureFile) {
    if (errorFile.exists()) {
      errorFile.delete();
    }
    if (failureFile.exists()) {
      failureFile.delete();
    }
  }

// Main Method. ----------------------------------------------------------------

  /**
   * Test suite main method.
   * <p>
   *   <tt>NOTE: Do not call this method!</tt>
   * </p>
   *
   * @param args main method arguments
   */
  public static void main(String[] args) {
    if (args != null) {
      if ((args.length == 5) && (args[0] != null) && (args[1] != null) &&
          (args[2] != null) && (args[3] != null) && (args[4] != null)) {
        execNativeTest(args[0], args[1], args[2], args[3], args[4]);
        return;
      }
      if ((args.length == 4) && (args[0] != null) && (args[1] != null) &&
          (args[2] != null) && (args[3] != null)) {
        execTest(args[0], args[1], args[2], args[3]);
        return;
      }
      if ((args.length == 1) &&
          TestEnvironment.STRING_JAVA_HOME.equalsIgnoreCase(args[0])) {
        System.out.println(TestEnvironment.getJavaHome()); // NOPMD
        return;
      }
    }
    System.exit(-1); // NOFB
  }

// Inner Classes. --------------------------------------------------------------

  /**
   * Input Stream Listener.
   */
  static class InputStreamListener extends Thread {
    private final BufferedReader isr;
    private final PrintWriter pw;
    private final StringWriter sw;
    private boolean stopListening;
    public InputStreamListener(InputStream inputStream) {
      setDaemon(true);
      isr = new BufferedReader(new InputStreamReader(inputStream));
      sw = new StringWriter();
      pw = new PrintWriter(sw);
    }
    public boolean hasContent() {
      return (sw.getBuffer().length() > 0);
    }
    public String getContent() {
      return sw.toString();
    }
    public void stopListening() {
      stopListening = true;
      interrupt();
    }
    public void run() {
      try {
        while (true) {
          if (stopListening) {
            return;
          }
          yield();
          for (String nl = isr.readLine(); nl != null; nl = isr.readLine()) {
            if (stopListening) {
              return;
            }
            pw.println(nl);
          }
        }
      } catch (IOException ioe) { // NOPMD
        // empty catch block
      } finally {
        pw.close();
      }
    }
  }

  /**
   * Test Listener.
   */
  static class ExecTestListener implements TestListener {
    private final TestResult testResult;
    private final String errorFileName;
    private final String failureFileName;
    private ObjectOutputStream eoos;
    private ObjectOutputStream foos;
    public ExecTestListener(TestResult testResult, String errorFileName,
                            String failureFileName) {
      this.testResult = testResult;
      this.errorFileName = errorFileName;
      this.failureFileName = failureFileName;
    }
    public void addError(Test test, Throwable t) {
      try {
        if (eoos == null) {
          eoos = new ObjectOutputStream(new FileOutputStream(errorFileName));
        }
        eoos.writeObject(t);
      } catch (IOException ioe) {
        testResult.stop();
        ioe.printStackTrace(); // NOPMD
      }
    }
    public void addFailure(Test test, AssertionFailedError e) {
      try {
        if (foos == null) {
          foos = new ObjectOutputStream(new FileOutputStream(failureFileName));
        }
        foos.writeObject(e);
      } catch (IOException ioe) {
        testResult.stop();
        ioe.printStackTrace(); // NOPMD
      }
    }
    public void endTest(Test test) {
      TestEnvironment.closeStream(eoos);
      TestEnvironment.closeStream(foos);
    }
    public void startTest(Test test) {
      // empty implementation
    }
  }

}
